var browser = (function (util, proxies, sanedomparsererror, theWindow) {
    "use strict";

    var module = {};

    var createHiddenElement = function (doc, tagName, width, height) {
        var element = doc.createElement(tagName);
        // 'display: none' doesn't cut it, as browsers seem to be lazy loading CSS
        element.style.visibility = "hidden";
        element.style.width = width + "px";
        element.style.height = height + "px";
        element.style.position = "absolute";
        element.style.top = -10000 - height + "px";
        element.style.left = -10000 - width + "px";
        // We need to add the element to the document so that its content gets loaded
        doc.getElementsByTagName("body")[0].appendChild(element);
        return element;
    };

    var wait = function (timeout) {
        if (timeout > 0) {
            return new Promise(function (resolve) {
                setTimeout(resolve, timeout);
            });
        } else {
            return Promise.resolve();
        }
    };

    module.executeJavascript = function (element, options) {
        return new Promise(function (resolve) {
            var iframe = createHiddenElement(
                    theWindow.document,
                    "iframe",
                    options.width,
                    options.height
                ),
                html = element.outerHTML,
                iframeErrorsMessages = [],
                executeJsTimeout = options.executeJsTimeout || 0;

            var cleanUp = function () {
                // We have to defer removing the iframe from the current document, see https://github.com/cburgmer/rasterizeHTML.js/issues/219
                theWindow.document
                    .getElementsByTagName("body")[0]
                    .removeChild(iframe);
            };

            var doResolve = function () {
                var doc = iframe.contentDocument;
                resolve({
                    document: doc,
                    errors: iframeErrorsMessages,
                    cleanUp: cleanUp,
                });
            };

            var xhr = iframe.contentWindow.XMLHttpRequest,
                finishNotifyXhrProxy = proxies.finishNotifyingXhr(xhr),
                baseUrlXhrProxy = proxies.baseUrlRespectingXhr(
                    finishNotifyXhrProxy,
                    options.baseUrl
                );

            iframe.onload = function () {
                wait(executeJsTimeout)
                    .then(finishNotifyXhrProxy.waitForRequestsToFinish)
                    .then(doResolve);
            };

            iframe.contentDocument.open();
            iframe.contentWindow.XMLHttpRequest = baseUrlXhrProxy;
            iframe.contentWindow.onerror = function (msg) {
                iframeErrorsMessages.push({
                    resourceType: "scriptExecution",
                    msg: msg,
                });
            };

            iframe.contentDocument.write("<!DOCTYPE html>");
            iframe.contentDocument.write(html);
            iframe.contentDocument.close();
        });
    };

    var createHiddenSandboxedIFrame = function (doc, width, height) {
        var iframe = doc.createElement("iframe");
        iframe.style.width = width + "px";
        iframe.style.height = height + "px";
        // 'display: none' doesn't cut it, as browsers seem to be lazy loading content
        iframe.style.visibility = "hidden";
        iframe.style.position = "absolute";
        iframe.style.top = -10000 - height + "px";
        iframe.style.left = -10000 - width + "px";
        // make sure content gets exact width independent of box-sizing value
        iframe.style.borderWidth = 0;
        // Don't execute JS, all we need from sandboxing is access to the iframe's document
        iframe.sandbox = "allow-same-origin";
        // Don't include a scrollbar on Linux
        iframe.scrolling = "no";
        return iframe;
    };

    var createIframeWithSizeAtZoomLevel1 = function (width, height, zoom) {
        var scaledViewportWidth = Math.floor(width / zoom),
            scaledViewportHeight = Math.floor(height / zoom);

        return createHiddenSandboxedIFrame(
            theWindow.document,
            scaledViewportWidth,
            scaledViewportHeight
        );
    };

    var calculateZoomedContentSizeAndRoundUp = function (
        actualViewport,
        requestedWidth,
        requestedHeight,
        zoom
    ) {
        return {
            width: Math.max(actualViewport.width * zoom, requestedWidth),
            height: Math.max(actualViewport.height * zoom, requestedHeight),
        };
    };

    var selectElementOrDescendant = function (element, selector) {
        var descendant = element.querySelector(selector);
        if (descendant) {
            return descendant;
        } else if (element.ownerDocument.querySelector(selector) === element) {
            return element;
        }

        throw {
            message: "Clipping selector not found",
        };
    };

    var calculateContentSize = function (
        rootElement,
        selector,
        requestedWidth,
        requestedHeight,
        zoom
    ) {
        // clientWidth/clientHeight needed for PhantomJS
        var actualViewportWidth = Math.max(
                rootElement.scrollWidth,
                rootElement.clientWidth
            ),
            actualViewportHeight = Math.max(
                rootElement.scrollHeight,
                rootElement.clientHeight
            ),
            top,
            left,
            originalWidth,
            originalHeight,
            rootFontSize,
            element,
            rect,
            contentSize;

        if (selector) {
            element = selectElementOrDescendant(rootElement, selector);

            rect = element.getBoundingClientRect();

            top = rect.top;
            left = rect.left;
            originalWidth = rect.width;
            originalHeight = rect.height;
        } else {
            top = 0;
            left = 0;
            originalWidth = actualViewportWidth;
            originalHeight = actualViewportHeight;
        }

        contentSize = calculateZoomedContentSizeAndRoundUp(
            {
                width: originalWidth,
                height: originalHeight,
            },
            requestedWidth,
            requestedHeight,
            zoom
        );

        rootFontSize = theWindow.getComputedStyle(
            rootElement.ownerDocument.documentElement
        ).fontSize;

        return {
            left: left,
            top: top,
            width: contentSize.width,
            height: contentSize.height,
            viewportWidth: actualViewportWidth,
            viewportHeight: actualViewportHeight,

            rootFontSize: rootFontSize,
        };
    };

    var findCorrelatingElement = function (element, documentClone) {
        var tagName = element.tagName;
        // Stupid but simple method: find first match. Should work for a single HTML element, and any other element given as root
        return documentClone.querySelector(tagName);
    };

    var elementToFullHtmlDocument = function (element) {
        var tagName = element.tagName.toLowerCase();
        if (tagName === "html" || tagName === "body") {
            return element.outerHTML;
        }

        // Simple hack: hide the body from sizing, otherwise browser would apply a 8px margin
        return '<body style="margin: 0;">' + element.outerHTML + "</body>";
    };

    module.calculateDocumentContentSize = function (element, options) {
        return new Promise(function (resolve, reject) {
            var zoom = options.zoom || 1,
                iframe;

            iframe = createIframeWithSizeAtZoomLevel1(
                options.width,
                options.height,
                zoom
            );
            // We need to add the element to the document so that its content gets loaded
            theWindow.document
                .getElementsByTagName("body")[0]
                .appendChild(iframe);

            iframe.onload = function () {
                var doc = iframe.contentDocument,
                    size;

                try {
                    size = calculateContentSize(
                        findCorrelatingElement(element, doc),
                        options.clip,
                        options.width,
                        options.height,
                        zoom
                    );

                    resolve(size);
                } catch (e) {
                    reject(e);
                } finally {
                    theWindow.document
                        .getElementsByTagName("body")[0]
                        .removeChild(iframe);
                }
            };

            // srcdoc doesn't work in PhantomJS yet
            iframe.contentDocument.open();
            iframe.contentDocument.write("<!DOCTYPE html>");
            iframe.contentDocument.write(elementToFullHtmlDocument(element));
            iframe.contentDocument.close();
        });
    };

    module.parseHtmlFragment = function (htmlFragment) {
        var doc = theWindow.document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = htmlFragment;

        var element = doc.querySelector("body").firstChild;

        if (!element) {
            throw "Invalid source";
        }

        return element;
    };

    var addHTMLTagAttributes = function (doc, html) {
        var attributeMatch = /<html((?:\s+[^>]*)?)>/im.exec(html),
            helperDoc =
                theWindow.document.implementation.createHTMLDocument(""),
            htmlTagSubstitute,
            i,
            elementSubstitute,
            attribute;

        if (!attributeMatch) {
            return;
        }

        htmlTagSubstitute = "<div" + attributeMatch[1] + "></div>";
        helperDoc.documentElement.innerHTML = htmlTagSubstitute;
        elementSubstitute = helperDoc.querySelector("div");

        for (i = 0; i < elementSubstitute.attributes.length; i++) {
            attribute = elementSubstitute.attributes[i];
            doc.documentElement.setAttribute(attribute.name, attribute.value);
        }
    };

    module.parseHTML = function (html) {
        // We should be using the DOMParser, but it is not supported in older browsers
        var doc = theWindow.document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = html;

        addHTMLTagAttributes(doc, html);
        return doc;
    };

    var failOnInvalidSource = function (doc) {
        try {
            return sanedomparsererror.failOnParseError(doc);
        } catch (e) {
            throw {
                message: "Invalid source",
                originalError: e,
            };
        }
    };

    module.validateXHTML = function (xhtml) {
        var p = new DOMParser(),
            doc = p.parseFromString(xhtml, "application/xml");

        failOnInvalidSource(doc);
    };

    var lastCacheDate = null;

    var getUncachableURL = function (url, cache) {
        if (cache === "none" || cache === "repeated") {
            if (lastCacheDate === null || cache !== "repeated") {
                lastCacheDate = Date.now();
            }
            return url + "?_=" + lastCacheDate;
        } else {
            return url;
        }
    };

    var doDocumentLoad = function (url, options) {
        return new Promise(function (resolve, reject) {
            var xhr = new window.XMLHttpRequest(),
                joinedUrl = util.joinUrl(options.baseUrl, url),
                augmentedUrl = getUncachableURL(joinedUrl, options.cache),
                doReject = function (e) {
                    reject({
                        message: "Unable to load page",
                        originalError: e,
                    });
                };

            xhr.addEventListener(
                "load",
                function () {
                    if (xhr.status === 200 || xhr.status === 0) {
                        resolve(xhr.responseXML);
                    } else {
                        doReject(xhr.statusText);
                    }
                },
                false
            );

            xhr.addEventListener(
                "error",
                function (e) {
                    doReject(e);
                },
                false
            );

            try {
                xhr.open("GET", augmentedUrl, true);
                xhr.responseType = "document";
                xhr.send(null);
            } catch (e) {
                doReject(e);
            }
        });
    };

    module.loadDocument = function (url, options) {
        return doDocumentLoad(url, options).then(function (doc) {
            return failOnInvalidSource(doc);
        });
    };

    return module;
})(util, proxies, sanedomparsererror, window);
